﻿using System;
using System.Collections.Generic;
using System.Linq;

namespace Creventive.Wpf.ShellFactory.Services
{
    /// <summary>
    ///   Provides a container for services.
    /// </summary>
    public class ServiceContainer : IServiceContainer
    {
        #region Static Fields/Constants

        /// <summary>
        /// Provides a default service container.
        /// </summary>
        public static IServiceContainer Default = new ServiceContainer();

        #endregion

        private Dictionary<Type, Stack<IServiceFactory>> factoryLookup = new Dictionary<Type, Stack<IServiceFactory>>();
        private IServiceContainer parent;

        /// <summary>
        ///   Initializes a new instance of the <see cref = "ServiceContainer" /> class.
        /// </summary>
        public ServiceContainer()
        {}

        /// <summary>
        ///   Initializes a new instance of the <see cref = "ServiceContainer" /> class.
        /// </summary>
        /// <param name = "parent">The parent.</param>
        public ServiceContainer(ServiceContainer parent)
        {
            this.parent = parent;
        }

        /// <summary>
        ///   Registers the service factory.
        /// </summary>
        /// <param name = "keyType">Type of the key.</param>
        /// <param name = "factory">The factory.</param>
        /// <param name = "promote">if set to <c>true</c> [promote].</param>
        public virtual void RegisterServiceFactory(Type keyType, IServiceFactory factory, bool promote = false)
        {
            this.EnsureKey(keyType);
            if (promote && this.parent != null)
                this.parent.RegisterServiceFactory(keyType, factory, promote);
            else
                this.factoryLookup[keyType].Push(factory);
        }

        /// <summary>
        ///   Unregisters the service.
        /// </summary>
        /// <param name = "keyType">Type of the service to manage.</param>
        /// <param name = "promoted">if set to <c>true</c>, the entire container ancestry will be searched until the first instance of the service is found, then it will be removed.</param>
        public virtual void UnregisterService(Type keyType, bool promoted)
        {
            if (this.factoryLookup.ContainsKey(keyType))
            {
                Stack<IServiceFactory> factoryStack = this.factoryLookup[keyType];
                if (factoryStack.Count > 0)
                    factoryStack.Pop();
            }
            else if (this.parent != null)
                this.parent.UnregisterService(keyType, promoted);
        }

        /// <summary>
        ///   Ensures the key type exists.
        /// </summary>
        /// <param name = "keyType">Type of the service to manage.</param>
        protected virtual void EnsureKey(Type keyType)
        {
            if (!this.factoryLookup.ContainsKey(keyType))
                this.factoryLookup[keyType] = new Stack<IServiceFactory>();
        }

        /// <summary>
        ///   Determines whether the specified service has been registered.
        /// </summary>
        /// <param name = "keyType">Type of the service to manage.</param>
        /// <returns>
        ///   <c>true</c> if the specified service has been registered; otherwise, <c>false</c>.
        /// </returns>
        public bool HasService(Type keyType)
        {
            return this.factoryLookup.ContainsKey(keyType);
        }

        /// <summary>
        ///   Registers the instance.
        /// </summary>
        /// <typeparam name = "T"></typeparam>
        /// <param name = "instance">The instance.</param>
        /// <param name = "lifetime">The lifetime.</param>
        /// <param name = "promote">if set to <c>true</c>, this service is registered at the lowest level ancestor container.</param>
        public void RegisterService<T>(T instance, ServiceLifetime lifetime, bool promote = false) where T : class
        {
            switch (lifetime)
            {
                case ServiceLifetime.ContainerLifetime:
                    this.RegisterServiceFactory(typeof(T), new ContainerLifetimeServiceFactory(instance), promote);
                    break;

                case ServiceLifetime.ObjectLifetime:
                    this.RegisterServiceFactory(typeof(T), new ObjectLifetimeServiceFactory(instance), promote);
                    break;

                default:
                    throw new ArgumentOutOfRangeException("lifetime");
            }
        }

        /// <summary>
        ///   Registers the object factory for the given key type, creating an object of the given instance type.
        /// </summary>
        /// <typeparam name = "Tkey">The type of the key.</typeparam>
        /// <typeparam name = "Tinstance">The type of the instance.</typeparam>
        /// <param name = "instantiationMode">The instantiation mode.</param>
        /// <param name = "promote">if set to <c>true</c>, this service is registered at the lowest level ancestor container.</param>
        public void RegisterServiceFactory<Tkey, Tinstance>(InstantiationMode instantiationMode, bool promote = false)
            where Tkey : class
            where Tinstance : Tkey, new()
        {
            this.RegisterServiceFactory(typeof(Tkey), typeof(Tinstance), instantiationMode, promote);
        }

        /// <summary>
        ///   Registers the object factory for the given key type, creating an object of the given instance type.
        /// </summary>
        /// <param name = "keyType">Type of the key.</param>
        /// <param name = "instanceType">Type of the instance.</param>
        /// <param name = "instantiationMode">The instantiation mode.</param>
        /// <param name = "promote">if set to <c>true</c>, this service is registered at the lowest level ancestor container.</param>
        public void RegisterServiceFactory(Type keyType, Type instanceType, InstantiationMode instantiationMode, bool promote = false)
        {
            if (keyType == null)
                throw new ArgumentNullException("keyType");
            if (instanceType == null)
                throw new ArgumentNullException("instanceType");
            if (instanceType.IsInterface || instanceType.IsAbstract)
                throw new ArgumentException(@"Instance Type must be instantiatable", "instanceType");
            if ((keyType.IsInterface && !instanceType.GetInterfaces().Contains(keyType)) || !keyType.IsAssignableFrom(instanceType))
                throw new ArgumentException(@"Instance Type must derive from the Key Type", "instanceType");
            this.RegisterServiceFactory(keyType, new InjectingServiceFactory(instanceType, instantiationMode), promote);
        }

        /// <summary>
        ///   Registers the object factory for the type returned by the delegate.
        /// </summary>
        /// <typeparam name = "T"></typeparam>
        /// <param name = "createInstance">The create instance.</param>
        /// <param name = "instantiationMode">The instantiation mode.</param>
        /// <param name = "promote">if set to <c>true</c>, this service is registered at the lowest level ancestor container.</param>
        public void RegisterServiceFactory<T>(Func<T> createInstance, InstantiationMode instantiationMode, bool promote = false) where T : class
        {
            this.RegisterServiceFactory(new LambdaServiceFactory<T>(createInstance, instantiationMode), promote);
        }

        /// <summary>
        ///   Registers the given object factory.
        /// </summary>
        /// <typeparam name = "T"></typeparam>
        /// <param name = "serviceFactory">The object factory.</param>
        /// <param name = "promote">if set to <c>true</c>, this service is registered at the lowest level ancestor container.</param>
        public void RegisterServiceFactory<T>(IServiceFactory<T> serviceFactory, bool promote = false) where T : class
        {
            this.RegisterServiceFactory(typeof(T), serviceFactory, promote);
        }

        /// <summary>
        ///   Attempts to resolve the given type. Returns null if no object was found.
        /// </summary>
        /// <typeparam name = "T">Type of the key.</typeparam>
        /// <returns></returns>
        public T GetService<T>() where T : class
        {
            return (T)this.GetService(typeof(T));
        }

        /// <summary>
        ///   Attempts to resolve the given type. Throws an InvalidOperationException if the service is missing.
        /// </summary>
        /// <param name = "serviceType"></param>
        /// <returns></returns>
        public object EnsureService(Type serviceType)
        {
            var service = this.GetService(serviceType);
            if (service == null)
                throw new InvalidOperationException("Could not retrieve the required service " + serviceType.FullName);
            else
                return service;
        }

        /// <summary>
        ///   Attempts to resolve the given type. Throws an InvalidOperationException if the service is missing.
        /// </summary>
        /// <typeparam name = "T"></typeparam>
        /// <returns></returns>
        public T EnsureService<T>()
        {
            return (T)this.EnsureService(typeof(T));
        }

        /// <summary>
        ///   Attempts to resolve the given type. Returns null if no object was found.
        /// </summary>
        /// <param name = "keyType">Type of the key.</param>
        /// <returns></returns>
        public object GetService(Type keyType)
        {
            if (keyType == typeof(IServiceContainer))
                return this;

            IServiceFactory serviceFactory = this.GetServiceFactory(keyType);
            if (serviceFactory != null)
                return serviceFactory.Create(this);
            return null;
        }

        /// <summary>
        ///   Creates a child container which can have its own services not available in its parent container, but which also can resolve every object its parent container can.
        /// </summary>
        /// <returns></returns>
        public IServiceContainer CreateChild()
        {
            return new ServiceContainer(this);
        }

        /// <summary>
        ///   Gets the service factory.
        /// </summary>
        /// <typeparam name = "T">The type of the service to manage.</typeparam>
        /// <returns></returns>
        public IServiceFactory<T> GetServiceFactory<T>() where T : class
        {
            return (IServiceFactory<T>)this.GetServiceFactory(typeof(T));
        }

        /// <summary>
        ///   Determines whether the specified service has been registered.
        /// </summary>
        /// <typeparam name = "T">The type of the service to manage.</typeparam>
        /// <returns>
        ///   <c>true</c> if the specified service has been registered; otherwise, <c>false</c>.
        /// </returns>
        public bool HasService<T>()
        {
            return this.HasService(typeof(T));
        }

        /// <summary>
        ///   Gets the service factory for the given key type.
        /// </summary>
        /// <param name = "keyType">Type of the service to manage.</param>
        /// <returns></returns>
        public IServiceFactory GetServiceFactory(Type keyType)
        {
            Stack<IServiceFactory> factory;
            if (this.factoryLookup.TryGetValue(keyType, out factory))
                return factory.Peek();

            if (this.parent != null)
                return this.parent.GetServiceFactory(keyType);

            return null;
        }
    }
}